Give your server an API with ShellBox

shellbox

View the project on Github :

Managing servers is not always easy. You have to balance reliability, security, and user requests for features.

One common issue arises when a regular user needs to be able perform some task on a server, such as restarting a service, changing a configuration, or updating a software, but you don’t trust them enough to give them an SSH access1. Of course, Linux provides a plethora of tools to help you create secure and restricted login on the system, but they can be a pain to setup, and too risky if you don’t know exactly how they work.

I found myself in this situation recently, on a server where we run some custom services (homemade web-apps and other self-hosted stuff), and I thought that it would be very handy to have… well, yet another custom service. One that exposes a simple HTTP API to run some predefined commands on the server, for instance commands that would fetch, rebuild and restart some other app, or commands that would return the current working status of something of the server, or… basically anything. It was a perfect fit for the Rust+Rocket combo that I enjoy a lot2, too, so this was the perfect excuse for a week-end project3.

Specs

After thinking about it a bit, I went with this list of requirements around two main constraints :

  • practicality :
    • the admin(s) should be able to specify any command they want (shell command or script/binary to launch), in a simple configuration file, to make them available
    • the app should provide a simple HTTP API to run these commands, such as POST /commands/update_my_service
    • the API should also send the output of the command back to the client, ideally in realtime
    • it shouldn’t be too difficult to also provide a user-friendly web-based UI that connects to this API, for the most non-technical users… we’ll see.
  • security :
    • reduce the exposed attack surface as much as possible : especially, strictly no way for users to provide arbitrary commands that are not exactly defined in the (static) config file
    • offer basic user management, with API keys that can be allowed to run only specific commands
    • Rust, being both a statically-typed compiled language, and a memory-safe one, offers already some pretty strong guarantees in itself here

After a bit of work, ShellBox / shbx was born. Why ShellBox? Because it’s like a soundbox for your shell, obviously. Of course, it’s all open-source and available on Github. Feel free to leave it a star if you find it interesting !

https://github.com/Foalyy/shbx

The app provides its own API documentation on the /api/doc and /api/rapidoc pages4, but an online version is available here as well :

https://shbx.silica.io/api/doc/
https://shbx.silica.io/api/rapidoc/

Examples

So, what can I use it for?

  • perform some common administration tasks :
    • apt update && apt upgrade
    • certbot renew
    • /root/backup_server.sh
    • reboot
  • manage existing services :
    • systemctl restart nginx
    • a2ensite my_dev_website && systemctl reload apache2
  • directly running services :
    • git pull && cargo build && cargo run
  • trigger another shbx instance on another server (maybe inside an internal, protected network) :
    • curl -L -H "X-API-Key: $API_KEY" -X POST "https://shbx.example.com/api/commands/reboot"

API

Once a command is launched, it can run in the background indefinitely as a task5. Depending on the endpoint you use to start a command, shbx will either :

  • execute the command synchronously, then send you back the result as a JSON structure when it’s done (useful for commands that execute quickly, for instance that return the status of something)
  • execute the command asynchronously, and send you back the result in realtime in an SSE-compatible text/event-stream in JSON format (useful when processing this output in a script)
  • execute the command asynchronously, and send you back the result in realtime in a user-friendly text/plain format (useful when manually calling the API through curl)

When launching a command asynchronously, there are API endpoints that you can use to in order to reconnect to the output of the task later. You can also send signals to the task, for instance to terminate or kill the process that it runs.

Here is an example of a command launched in async text/plain mode, using curl, with realtime output :

$ curl -L -N -H "X-API-Key: $API_KEY" -X POST "https://shbx.example.com/api/commands/rebuild_my_app/stream/text"
[shbx] Task started with id 1cf253f3-16f3-4abe-b853-ca42c04f2c66
   Compiling proc-macro2 v1.0.63
   Compiling quote v1.0.29
   Compiling unicode-ident v1.0.10
   Compiling autocfg v1.1.0
   Compiling version_check v0.9.4
...
   Compiling tera v1.19.0
   Compiling rocket_dyn_templates v0.1.0-rc.3
   Compiling my_app v0.1.0 (/home/john/projects/my_app)
    Finished dev [unoptimized + debuginfo] target(s) in 30.75s
[shbx] Task exited with exit code 0
[shbx] Task 1cf253f3-16f3-4abe-b853-ca42c04f2c66 terminated after 31126ms

For more examples of API requests, take a look at the README on Github.

Web UI

For users less inclined to using the command-line and curl directly, a simple and user-friendly web UI is provided. After logging in, it can be used to list available commands and running tasks :

And to launch commands or view the output of tasks launched previously :

Final word

If you have an interesting application for ShellBox, please share it in the comments below !

Footnotes
  1. Assuming they even know what SSH is, obviously. ↩︎
  2. Some of these custom services I was talking about are built with this, but more on this in another article. ↩︎
  3. Of course, this “week-end project” lasted two weeks, but hey, you know how time flies. ↩︎
  4. Same content, different interface ↩︎
  5. By default, tasks will be killed after running for 10 seconds, but this timeout can be customized or disabled entirely for each command individually. ↩︎

Leave a Reply

Your email address will not be published. Required fields are marked *